Skip to content

Add evdev hotkey profile modifiers for per-recording post-processing#293

Open
materemias wants to merge 3 commits intomainfrom
feature/hotkey-profile-modifiers
Open

Add evdev hotkey profile modifiers for per-recording post-processing#293
materemias wants to merge 3 commits intomainfrom
feature/hotkey-profile-modifiers

Conversation

@materemias
Copy link
Copy Markdown
Collaborator

Note: Reopened from #290, which was closed when the fork was deleted. This PR contains the same changes, rebased onto current main.

Summary

  • Add [hotkey.profile_modifiers] config for evdev hotkey detection: map modifier keys to named profiles so holding a modifier while pressing the hotkey activates a different post-processing pipeline
  • Reuses the existing profile override file mechanism (same as CLI --profile flag), so all profile features (custom post-process command, timeout, output mode) work automatically
  • Adds cleanup on error/shutdown paths and warns at startup if a profile modifier key overlaps with required modifiers

Example config:

[hotkey]
key = "SCROLLLOCK"
enabled = true

[hotkey.profile_modifiers]
RIGHTSHIFT = "translate"

[profiles.translate]
post_process_command = "my-cleanup.sh --translate-en"
timeout_ms = 10000

Bare ScrollLock uses default post-processing; Right Shift + ScrollLock translates to English.

Only applies to evdev hotkey detection (enabled = true). When using compositor keybindings, use voxtype record start --profile <name> instead.

Files changed

  • src/config.rs — New profile_modifiers field on HotkeyConfig + 2 tests
  • src/hotkey/mod.rsprofile_override field on HotkeyEvent::Pressed
  • src/hotkey/evdev_listener.rs — Parse, track, and emit profile modifier state
  • src/daemon.rs — Write profile override file from hotkey event; cleanup on error/shutdown/startup
  • docs/CONFIGURATION.md — New [hotkey.profile_modifiers] section
  • docs/USER_MANUAL.md — Profile modifiers subsection under hotkeys
  • config/default.toml — Commented example

Test plan

  • cargo test — 549 tests pass (including 2 new profile_modifiers config tests)
  • cargo clippy — no new warnings
  • Manual test: Shift+ScrLock activates translate profile, bare ScrLock uses default

🤖 Generated with Claude Code

materemias and others added 2 commits April 1, 2026 16:24
Allow modifier keys to activate named profiles when held during hotkey
press. This enables different post-processing pipelines (e.g., translate
to English) without separate keybindings or compositor support.

New config option [hotkey.profile_modifiers] maps evdev key names to
profile names. The profile override file mechanism is reused from the
existing CLI --profile flag, so all existing profile features (custom
post-process command, timeout, output mode) work automatically.

Includes cleanup on error/shutdown paths and validation warning when
profile modifier keys overlap with required modifiers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add profile_modifiers to configuration guide, user manual, and default
config file with examples and usage notes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 1, 2026 14:54
@materemias materemias requested a review from peteonrails as a code owner April 1, 2026 14:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for selecting a post-processing profile based on a held modifier key when using evdev hotkey detection, by propagating a profile_override through the hotkey event and reusing the existing profile_override runtime-file mechanism used by the CLI --profile flag.

Changes:

  • Add [hotkey.profile_modifiers] config to map modifier keys to profile names.
  • Emit HotkeyEvent::Pressed { profile_override } from the evdev listener based on held profile-modifier state.
  • Have the daemon write/cleanup the profile_override runtime file based on hotkey events; update docs and defaults with examples.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/config.rs Adds HotkeyConfig.profile_modifiers plus parsing tests.
src/hotkey/mod.rs Extends HotkeyEvent::Pressed to include profile_override.
src/hotkey/evdev_listener.rs Parses profile modifiers, tracks held state, and emits profile override on hotkey press.
src/daemon.rs Writes/cleans profile_override file based on hotkey events and shutdown/error paths.
docs/CONFIGURATION.md Documents new [hotkey.profile_modifiers] section with examples.
docs/USER_MANUAL.md Adds a “Profile Modifiers” subsection under hotkeys with example config.
config/default.toml Adds commented examples for model_modifier and hotkey.profile_modifiers.

Comment on lines +505 to +516
// Track profile modifier state
if let Some(profile_name) = profile_modifiers.get(&key) {
match value {
1 => active_profile_modifier = Some(profile_name.clone()),
0 => {
if active_profile_modifier.as_deref() == Some(profile_name.as_str()) {
active_profile_modifier = None;
}
}
_ => {}
}
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

active_profile_modifier only tracks a single profile name, which breaks when multiple profile modifier keys are held (e.g., press A then B, release B => profile clears even if A is still held). Consider tracking held profile-modifier keys (e.g., a HashSet<Key> or counts) and deriving the active profile deterministically at hotkey-press time (including a defined precedence when multiple are held).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b811b0b. Replaced the single Option<String> with HashMap<Key, String> that tracks all held profile modifier keys independently. Releasing one key only removes that key's entry, so other held modifiers are unaffected.

Comment on lines +405 to +407
// Track which profile modifier is currently held
let mut active_profile_modifier: Option<String> = None;

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

active_profile_modifier is introduced as additional listener state, but it is not cleared in the same places where other state is cleared (device change handling and stale-device validation). This can leave a “sticky” profile override after devices are re-enumerated/removed. Reset this state alongside active_modifiers, model_modifier_held, and is_pressed.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b811b0b. Added held_profile_modifiers.clear() in both device-change paths (inotify detection and periodic stale-device validation), alongside the existing active_modifiers/model_modifier_held/is_pressed resets.

Comment on lines +261 to +267
[profiles.translate]
post_process_command = "my-script.sh --translate-en"
timeout_ms = 10000

[profiles.formal]
post_process_command = "my-script.sh --formal"
```
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example sets timeout_ms inside [profiles.translate], but the profile field is post_process_timeout_ms (while timeout_ms belongs under [output.post_process]). As written, this timeout won’t apply to the profile and will likely be ignored by config parsing. Update the example (and surrounding text if needed) to use post_process_timeout_ms for profiles.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b811b0b. Changed to post_process_timeout_ms.


[profiles.translate]
post_process_command = "my-cleanup.sh --translate-en"
timeout_ms = 10000
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses timeout_ms under [profiles.translate], but profile timeouts are configured via post_process_timeout_ms (whereas timeout_ms is for [output.post_process]). This example currently won’t set the intended profile-specific timeout.

Suggested change
timeout_ms = 10000
post_process_timeout_ms = 10000

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b811b0b. Changed to post_process_timeout_ms.

src/daemon.rs Outdated
tracing::info!("Starting voxtype daemon");

// Clean up any stale cancel file from previous runs
// Clean up any stale override/cancel files from previous runs
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The startup comment says “stale override/cancel files,” but this block only cleans cancel and profile_override (not other override files like output_mode_override, model_override, or *_override). Either narrow the comment to match what’s actually cleaned up, or extend the cleanup to cover all override files that should not persist across daemon restarts.

Suggested change
// Clean up any stale override/cancel files from previous runs
// Clean up any stale cancel and profile override files from previous runs

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b811b0b. Narrowed the comment to match what's actually cleaned up.

Replace single-value active_profile_modifier with a HashMap of held
keys so releasing one modifier doesn't clear another's profile.
Reset held_profile_modifiers on device re-enumeration and stale device
removal. Fix doc examples using wrong field name (timeout_ms ->
post_process_timeout_ms). Narrow daemon startup cleanup comment to
match actual behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants